package cn.dansj.common.utils.transfer;

import cn.dansj.common.utils.enums.GlobalEnv;
import cn.dansj.common.utils.json.AutoDeserialize;
import cn.dansj.common.utils.json.JSONUtils;
import cn.dansj.common.utils.time.FastDateFormat;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;

import java.io.Serializable;
import java.sql.Timestamp;
import java.text.ParseException;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.temporal.*;
import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Stream;

@AutoDeserialize
public class DateTime implements Serializable, Comparable<DateTime> {
    private static final String DATE_REGEX = "^(?:(\\d{4})([-./])(0?[1-9]|1[0-2])\\2(0?[1-9]|[12][0-9]|3[01])|(\\d{4})(0[1-9]|1[0-2])(0[1-9]|[12][0-9]|3[01]))(?:[T\\s]([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(?:\\.\\d{1,9})?Z?)?$";
    private static final Pattern DATE_PATTERN = Pattern.compile(DATE_REGEX);
    private static final WeekFields WEEK_FIELDS = WeekFields.of(Locale.getDefault());
    private static final ZoneId defaultZoneId = ZoneId.systemDefault();

    private static final long serialVersionUID = GlobalEnv.SerialVersionUID;

    public static final int YEAR = 1;
    public static final int MONTH = 2;
    public static final int DAY = 5;

    public static final TemporalUnit YEARS = ChronoUnit.YEARS;
    public static final TemporalUnit MONTHS = ChronoUnit.MONTHS;
    public static final TemporalUnit DAYS = ChronoUnit.DAYS;
    public static final TemporalUnit HOURS = ChronoUnit.HOURS;
    public static final TemporalUnit MINUTES = ChronoUnit.MINUTES;
    public static final TemporalUnit SECONDS = ChronoUnit.SECONDS;
    public static final TemporalUnit WEEKS = ChronoUnit.WEEKS;

    private final ZonedDateTime zonedDateTime;
    private final LocalDate localDate;

    // 私有构造器
    private DateTime(ZonedDateTime zonedDateTime) {
        this.zonedDateTime = zonedDateTime;
        this.localDate = null;
        checkLocalDateAndZoneDateTime();
    }

    private DateTime(LocalDate localDate) {
        this.localDate = localDate;
        this.zonedDateTime = null;
        checkLocalDateAndZoneDateTime();
    }

    private void checkLocalDateAndZoneDateTime() {
        if (localDate == null && zonedDateTime == null) {
            throw new DateTimeException("时间不能为空");
        }
    }

    @JsonCreator
    public static DateTime getInstance() {
        return new DateTime(ZonedDateTime.now());
    }

    @JsonCreator
    public static DateTime getInstance(ZoneId zone) {
        return new DateTime(ZonedDateTime.now(zone));
    }

    @JsonCreator
    public static DateTime getInstance(Date date) {
        return new DateTime(ZonedDateTime.ofInstant(date.toInstant(), defaultZoneId));
    }

    @JsonCreator
    public static DateTime getInstance(Calendar calendar) {
        return getInstance(calendar.getTime());
    }

    @JsonCreator
    public static DateTime getInstance(Instant instant) {
        return new DateTime(ZonedDateTime.ofInstant(instant, defaultZoneId));
    }

    @JsonCreator
    public static DateTime getInstance(long epochMilli) {
        return new DateTime(ZonedDateTime.ofInstant(Instant.ofEpochMilli(epochMilli), defaultZoneId));
    }

    @JsonCreator
    public static DateTime getInstance(int year, int month, int day) {
        return new DateTime(LocalDate.of(year, month, day));
    }

    private static String removeQuoted(String text) {
        return Verification.isNotQuoted(text) ? text : StringUtils.substr(text, 1, -1);
    }

    @JsonCreator
    public static DateTime getInstance(String time) {
        time = removeQuoted(time);
        try {
            if (Verification.checkNull(time)) throw new DateTimeParseException("时间字符串不能为空");
            // 根据字符串特征选择对应的格式
            String format = time.contains("T") ? getDatePart(time.split("T")[0]) + (time.endsWith("Z") ? "'T'HH:mm:ss.SSS'Z'" : time.contains(".") ? "'T'HH:mm:ss.SSS" : "'T'HH:mm:ss")
                    : time.contains(" ") ? getDatePart(time.split(" ")[0]) + (time.split(" ")[1].contains(".") ? " HH:mm:ss.SSS" : " HH:mm:ss") : getDatePart(time.split(" ")[0]);
            return getInstance(FastDateFormat.getInstance(format).parse(time));
        } catch (ParseException exception) {
            throw new DateTimeParseException("时间解析失败：" + time);
        }
    }

    private static String getDatePart(String value) {
        return Stream.of(".", "-", "/").filter(value::contains).findFirst().map(sep -> "yyyy" + sep + "MM" + sep + "dd").orElse("yyyyMMdd");
    }

    public static boolean isDateTime(String time) {
        return DATE_PATTERN.matcher(removeQuoted(time)).matches();
    }

    public static String stringTime(String format, Date time) {
        return GetTime.stringTime(format, time);
    }

    @Override
    public String toString() {
        String timeString = toString("yyyy-MM-dd HH:mm:ss.SSS");
        if (timeString.endsWith(".000")) timeString = timeString.replace(".000", "");
        if (timeString.endsWith("00:00:00")) timeString = timeString.replace("00:00:00", "");
        return timeString.trim();
    }

    public String toString(String pattern) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
        return zonedDateTime != null ? zonedDateTime.format(formatter) : localDate.format(formatter);
    }

    public DateTime endOfDay() {
        return new DateTime(zonedDateTime != null ? zonedDateTime.with(LocalTime.MAX) : localDate.atTime(LocalTime.MAX).atZone(defaultZoneId));
    }

    public DateTime beginOfDay() {
        return new DateTime(zonedDateTime != null ? zonedDateTime.with(LocalTime.MIN) : localDate.atStartOfDay(defaultZoneId));
    }

    public DateTime addTime(long amount, TemporalUnit unit) {
        return zonedDateTime != null ? new DateTime(zonedDateTime.plus(amount, unit)) : new DateTime(localDate.plus(amount, unit));
    }

    public int getFullYear() {
        return zonedDateTime != null ? zonedDateTime.getYear() : localDate.getYear();
    }

    public DateTime addDay(long day) {
        return addTime(day, ChronoUnit.DAYS);
    }

    public DateTime addSecond(long seconds) {
        return new DateTime(Transformation.requireNonNullElseGet(zonedDateTime, () -> localDate.atStartOfDay(defaultZoneId)).plusSeconds(seconds));
    }

    public DateTime addMinute(long minutes) {
        return new DateTime(Transformation.requireNonNullElseGet(zonedDateTime, () -> localDate.atStartOfDay(defaultZoneId)).plusMinutes(minutes));
    }

    public DateTime addHour(long hours) {
        return new DateTime(Transformation.requireNonNullElseGet(zonedDateTime, () -> localDate.atStartOfDay(defaultZoneId)).plusHours(hours));
    }

    public DateTime setTime(int hour, int minute, int second, int nano) {
        return new DateTime(zonedDateTime != null ? zonedDateTime.withHour(hour).withMinute(minute).withSecond(second).withNano(nano) : localDate.atTime(hour, minute, second, nano).atZone(defaultZoneId));
    }

    public DateTime trunc() {
        return zonedDateTime != null ? new DateTime(zonedDateTime.truncatedTo(ChronoUnit.DAYS)) : this;
    }

    public DateTime trunc(int level) {
        return zonedDateTime != null ? truncZonedDateTime(level) : truncLocalDate(level);
    }

    private DateTime truncZonedDateTime(int level) {
        switch (level) {
            case YEAR:
                return new DateTime(zonedDateTime.withMonth(1).withDayOfMonth(1).truncatedTo(ChronoUnit.DAYS));
            case MONTH:
                return new DateTime(zonedDateTime.withDayOfMonth(1).truncatedTo(ChronoUnit.DAYS));
            default:
                return trunc();
        }
    }

    private DateTime truncLocalDate(int level) {
        switch (level) {
            case YEAR:
                return new DateTime(LocalDate.of(localDate.getYear(), 1, 1));
            case MONTH:
                return new DateTime(LocalDate.of(localDate.getYear(), localDate.getMonth(), 1));
            default:
                return this;
        }
    }

    public int getDayOfWeek() {
        return (zonedDateTime != null ? zonedDateTime.getDayOfWeek() : localDate.getDayOfWeek()).getValue();
    }

    public int getMonth() {
        return zonedDateTime != null ? zonedDateTime.getMonthValue() : localDate.getMonthValue();
    }

    public int getDay() {
        return zonedDateTime != null ? zonedDateTime.getDayOfMonth() : localDate.getDayOfMonth();
    }

    public DateTime endOfYear() {
        return zonedDateTime != null ? new DateTime(zonedDateTime.with(TemporalAdjusters.lastDayOfYear()).with(LocalTime.MAX)) : new DateTime(localDate.with(TemporalAdjusters.lastDayOfYear()));
    }

    public DateTime beginOfYear() {
        return zonedDateTime != null ? new DateTime(zonedDateTime.with(TemporalAdjusters.firstDayOfYear()).with(LocalTime.MIN)) : new DateTime(localDate.with(TemporalAdjusters.firstDayOfYear()));
    }

    public int weekOfMonth() {
        return zonedDateTime != null ? zonedDateTime.get(WEEK_FIELDS.weekOfMonth()) : localDate.get(WEEK_FIELDS.weekOfMonth());
    }

    public int weekOfYear() {
        return zonedDateTime != null ? zonedDateTime.get(WEEK_FIELDS.weekOfYear()) : localDate.get(WEEK_FIELDS.weekOfYear());
    }

    public int dayOfYear() {
        return zonedDateTime != null ? zonedDateTime.getDayOfYear() : localDate.getDayOfYear();
    }

    public String stringTimestamp() {
        return toString("yyyy-MM-dd HH:mm:ss.SSS");
    }

    // 转换方法
    public Date toDate() {
        return Date.from(toInstant());
    }

    public Timestamp toTimestamp() {
        return zonedDateTime != null ? Timestamp.from(zonedDateTime.toInstant()) : Timestamp.valueOf(localDate.atStartOfDay());
    }

    public Instant toInstant() {
        return Transformation.requireNonNullElseGet(zonedDateTime, () -> localDate.atStartOfDay(defaultZoneId)).toInstant();
    }

    public static class DateTimeParseException extends RuntimeException {
        public DateTimeParseException(String message) {
            super(message);
        }
    }

    public static Date toDate(Object obj) {
        if (obj == null) {
            return null;
        }

        if (obj instanceof DateTime) {
            return ((DateTime) obj).toDate();
        }

        if (obj instanceof Date) {
            return (Date) obj;
        }

        if (obj instanceof Instant) {
            return new Date(((Instant) obj).toEpochMilli());
        }

        if (obj instanceof ZonedDateTime) {
            return new Date(((ZonedDateTime) obj).toInstant().toEpochMilli());
        }

        if (obj instanceof LocalDate) {
            return new Date(((LocalDate) obj).atStartOfDay(defaultZoneId).toInstant().toEpochMilli());
        }

        if (obj instanceof LocalDateTime) {
            return new Date(((LocalDateTime) obj).atZone(defaultZoneId).toInstant().toEpochMilli());
        }

        if (obj instanceof String) {
            return getInstance((String) obj).toDate();
        }

        if (obj instanceof Long || obj instanceof Integer) {
            return new Date(((Number) obj).longValue());
        }

        if (obj instanceof Map) {
            Object date = ((Map<?, ?>) obj).get("$date");
            if (date instanceof String) {
                return getInstance((String) date).toDate();
            }
        }

        throw new ClassCastException("can not cast to Date from " + obj.getClass());
    }

    public static Instant toInstant(Object obj) {
        if (obj == null) {
            return null;
        }

        if (obj instanceof DateTime) {
            return ((DateTime) obj).toInstant();
        }

        if (obj instanceof Instant) {
            return (Instant) obj;
        }

        if (obj instanceof Date) {
            return ((Date) obj).toInstant();
        }

        if (obj instanceof ZonedDateTime) {
            return ((ZonedDateTime) obj).toInstant();
        }

        if (obj instanceof String) {
            String str = (String) obj;
            if (str.isEmpty() || "null".equals(str)) {
                return null;
            }
            return JSONUtils.toObj(str, Instant.class);
        }

        if (obj instanceof Map<?, ?>) return createInstant((Map<?, ?>) obj);

        throw new ClassCastException("can not cast to Date from " + obj.getClass());
    }

    private static Instant createInstant(Map<?, ?> map) {
        Number nano = (Number) map.get("nano");
        Number epochSecond = (Number) map.get("epochSecond");

        if (nano != null && epochSecond != null) {
            return Instant.ofEpochSecond(epochSecond.longValue(), nano.longValue());
        }

        if (epochSecond != null) {
            return Instant.ofEpochSecond(epochSecond.longValue());
        }

        Number epochMilli = (Number) map.get("epochMilli");
        if (epochMilli != null) {
            return Instant.ofEpochMilli(epochMilli.longValue());
        }

        throw new RuntimeException("can not create instant.");
    }

    @JsonValue
    public long getTimeInMillis() {
        return toInstant().toEpochMilli();
    }

    @Override
    public int compareTo(DateTime other) {
        return Long.compare(getTimeInMillis(), other.getTimeInMillis());
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        DateTime dateTime = (DateTime) o;
        return getTimeInMillis() == dateTime.getTimeInMillis();
    }

    @Override
    public int hashCode() {
        return Long.hashCode(getTimeInMillis());
    }
}